Option Explicit

' ======================================================================
' Field
' ======================================================================
Private fieldSheet As Worksheet
Private fieldBook As Workbook

' 写入内容单元的 sectionName 时的列号
' 默认值 = 1 ，默认写入 A 列
Private fieldSectionNameColNo As Integer

' 内容单元的每行内容从哪一列开始写
' 默认值 = 2 ，默认从 B 列开始写
Private sectionContentStartColNo As Integer

' ======================================================================
' 初始化方法
' ======================================================================
Private Sub Class_Initialize()
    fieldSectionNameColNo = 1
    sectionContentStartColNo = 2
End Sub

Private Sub Class_Terminate()
    Set fieldSheet = Nothing
    Set fieldBook = Nothing
End Sub

' ======================================================================
' Init
' ======================================================================
' 通过 Workbook 对象 + sheet 名来初始化
'
'    如果参数 `wbook` 为 null，将会抛出异常，异常码 `59802`
'    如果参数 `shName` 是空字符串，将会抛出异常，异常码 `59804`
'    如果参数 `shName` 指定的 sheet 名称在 `wbook` 中不存在，并且 `needCreateSheet==false`，将会抛出异常，异常码 `59801`
' @param wbook 需要操作的 sheet 所在的 Workbook
' @param shName 需要操作的 sheet 名称
' @optional-param needCreateSheet 如果在 `wbook` 中不存在名称为 `shName` 的 sheet，是否需要主动创建
'    默认值 = true，将会主动创建指定名称的 sheet
Public Sub InitBySheetName(ByRef wbook As Workbook, ByRef shName As String, Optional ByVal needCreateSheet As Boolean = true)
    ' 1. 检查参数
    ' 1.1 如果参数 `wbook` 为 null，抛出异常
    If wbook Is Nothing Then
        Err.Raise 59802, Description:="SheetReaderWriter.InitBySheetName: Workbook object is Nothing"
    End If

    ' 1.2 如果参数 `shName` 是空字符串，抛出异常
    If shName = "" Then
        Err.Raise 59804, Description:="SheetReaderWriter.InitBySheetName: param[shName] is empty string"
    End If

    ' 1.3 如果参数 `shName` 指定的 sheet 名称在 `wbook` 中不存在，并且 `needCreateSheet==false`
    '     抛出异常
    Dim hasSh As Boolean
    hasSh = UtilsWorkbook.hasSheet(wbook, shName)
    If hasSh = False Then
        If needCreateSheet = False Then
            Err.Raise 59801, Description:="SheetReaderWriter.InitBySheetName: Not exist sheet ["& shName & "] in Workbook[" & wbook.FullName & "]"
        End If
    End If

    ' 2. 创建当前对象
    ' 2.1 保存 wbook
    Set fieldBook = wbook

    ' 2.2 保存 sheet
    If hasSh = False Then
        ' 如果指定名称的 sheet 不存在，则需要主动创建
        Set fieldSheet = UtilsWorkbook.addLastTextSheet(fieldBook, shName)
    Else
        ' 从 workbook 中获取指定名称的 sheet 对象
        Set fieldSheet = fieldBook.Sheets(shName)
    End If
End Sub

' 通过 Workbook 对象 + Worksheet 对象来初始化
'
'    如果参数 `wsheet` 为 null，将会抛出异常，异常码 `59803`
'
' @param wsheet 需要操作的 sheet 对象
Public Sub InitBySheet(ByRef sh As Worksheet)
    ' 1. 检查参数
    ' 如果参数 `wsheet` 为 null，抛出异常
    If sh Is Nothing Then
        Err.Raise 59803, Description:="SheetReaderWriter.InitBySheet: Worksheet param[sh] is Nothing"
    End If

    ' 2. 创建当前对象
    ' 2.1 保存 wbook
    Set fieldBook = sh.Parent

    ' 2.2 保存 sheet
    Set fieldSheet = sh
End Sub

' ======================================================================
' Property
' ======================================================================
' 获取 sheet 所在的 Workbook 对象
Public Property Get Book() As Workbook
    Set Book = fieldBook
End Property

' 获取 Workbook 的完整路径
Public Property Get BookPath() As String
    BookPath = fieldBook.FullName
End Property

' 获取被操作的 sheet 对象
Public Property Get Sheet() As Worksheet
    Set Sheet = fieldSheet
End Property

' 获取被操作的 sheet 对象的名称
Public Property Get Name() As String
    Name = fieldSheet.Name
End Property

' 获取: 写入内容单元的 key 时的列号
Public Property Get SectionNameColNo() As Integer
    SectionNameColNo = fieldSectionNameColNo
End Property

' 设置: 写入内容单元的 key 时的列号
Public Property Let SectionNameColNo(ByVal v As Integer)
    fieldSectionNameColNo = v
End Property

' 获取: 内容单元的每行内容从哪一列开始写
Public Property Get ContentStartColNo() As Integer
    ContentStartColNo = sectionContentStartColNo
End Property

' 设置: 内容单元的每行内容从哪一列开始写
Public Property Let ContentStartColNo(ByVal v As Integer)
    sectionContentStartColNo = v
End Property

' 获取最后一个内容单元的起始行号
Public Property Get LastSectionStartRowNo() As Long
    LastSectionStartRowNo = UtilsWorksheet.getEndRowNoByColNo(fieldSheet, fieldSectionNameColNo)
End Property

' ======================================================================
' Function
' ======================================================================
' ==================================================
' 清空 sheet
' ==================================================
' 清空 sheet 中的所有内容（单元格、格式、公式、颜色等所有内容）
Public Sub clearAll()
    UtilsWorksheet.clearAll fieldSheet
End Sub

' 清空 sheet 中的所有内容（单元格、格式、公式、颜色等所有内容），并将 sheet 的所有单元格设置为字符串格式
Public Sub clearAllToTextSheet()
    UtilsWorksheet.clearAllAndToText fieldSheet
End Sub

' ==================================================
'  sheet 内搜索
' ==================================================
' 检查名称为 `sectionName` 的内容单元是否存在
'
' @param sectionName 内容单元名
' @return 内容单元是否存在
Public Function existSection(ByRef sectionName As String) As Boolean
    ' 1. 检查参数
    ' 1.1 如果 sectionName 是空字符串，则默认返回 false
    If sectionName = "" Then
        Exit Function
    End If

    ' 2. 在 sectionName 所在的列进行检查
    Dim r As Range
    Set r = fieldSheet.Columns(fieldSectionNameColNo) _
        .Find(what:=sectionName, lookat:=xlWhole)

    ' 3. 如果 r 不是空，则内容单元存在
    existSection = Not r Is Nothing
End Function

' 获取指定列中的最后一个被使用的单元格的行号，如果这一列没有被使用，则会返回 `-1`
'
' @param targetColumn 指定列的 [数值型列号] 或者 [字符串型列名]
' @return targetColumn 列中的最后一个被使用的单元格的行号
Public Function getColLastUsedRowNo(ByRef targetColumn as Variant) As Long
    Dim colNo As Integer
    colNo = UtilsWorksheet.convertVariantColToColNo(targetColumn)

    getColLastUsedRowNo = UtilsWorksheet.getEndRowNoByColNo(fieldSheet, colNo)

    ' 如果返回的行号是 1 ，则继续需要检查这个单元格是不是空
    If getColLastUsedRowNo = 1 Then
        If sheet.Cells(getColLastUsedRowNo, colNo).Value = "" Then
            getColLastUsedRowNo = -1
        End If
    End If
End Function

' 在指定列中，搜索目标字符串的开始位置
' @param colNo 限定搜索范围的列号，从 1 开始
'              如果参数 `<= 0`，或者 `> UtilsWorksheet.MAX_COL_NO`，将会抛出异常，异常码 `59805`
' @param str 需要搜索的字符串
' @return 字符串所在的行号
'         如果无法在指定列上搜索到字符串，则返回 -1
'         如果 `str` 是空字符串，则默认返回 `-1`
Public Function findStrInCol(ByRef targetColumn As Variant, ByRef str As String) As Long
    ' 1. 检查参数
    ' 1.1 检查 targetColumn 并转换
    Dim colNo As Integer
    colNo = UtilsWorksheet.convertVariantColToColNo(targetColumn)

    ' 1.2 如果 str 是空字符串，则默认返回 -1
    If str = "" Then
        findStrInCol = -1
        Exit Function
    End If

    ' 2. 在指定列进行检查
    Dim r As Range
    Set r = fieldSheet.Columns(colNo) _
        .Find(what:=str, lookat:=xlWhole)

    If r Is Nothing Then
        findStrInCol = -1
    Else
        findStrInCol = r.Row
    End If

End Function


' 目标 section 所在的内容单元的开始行号
'     开始位置 就是 sectionName 在 `SectionNameColNo` 列的第一次出现的行号
'
' @param sectionName 需要搜索的内容单元名
' @return sectionName 所在的内容单元的开始行号
'             行号从 1 开始
'             如果没有找到对应的 sectionName，则默认返回 `-1`
'             如果 `sectionName` 是空字符串，则默认返回 `-1`
Public Function getSectionUsageStartRowNo(ByRef sectionName As String) As Long
    ' 1. 检查参数
    ' 1.1 如果 sectionName 是空字符串，则默认返回 -1
    If sectionName = "" Then
        getSectionUsageStartRowNo = -1
        Exit Function
    End If

    ' 2. 在 sectionName 所在的列进行检查
    Dim r As Range
    Set r = fieldSheet.Columns(fieldSectionNameColNo) _
        .Find(what:=sectionName, lookat:=xlWhole)

    ' 2.1 搜索指定内容所在的起始行号
    If r Is Nothing Then
        ' 如果无法在指定列上搜索到 sectionName，则返回 -1
        getSectionUsageStartRowNo = -1
        Exit Function
    End If

    getSectionUsageStartRowNo = r.Row
End Function

' sectionName 所在的内容单元的开始行号和结束行号
'    开始位置 就是 sectionName 在 `SectionNameColNo` 列的第一次出现的行号
'    这里会从其开始行的下一行开始搜索，下一个非空单元格的前一行作为结束行号
'    如下，搜索的字符串为 `str1`，则返回的结果为 `PrimitivePair{10, 12}`
'        ```
'        行号  列N
'        10    str1  <<<< 从这里开始
'        11
'        12          <<<< 到这里结束
'        13    str2
'        ```
' @param sectionName 需要搜索的内容单元名
' @return PrimitivePair{Long 起始行号, Long 结束行号}
'             如果无法在指定列上搜索到字符串，则: `起始行号 = -1` ，`结束行号 = -1`
'             如果`起始行号`就是整个 sheet 中的最后一行，则将会设置 `结束行号 = 起始行号`
'             如果从 `起始行号` 行开始，指定列上之后的行都没有存在字符串的单元格了，将会设置 `结束行号 = sheet已使用范围的最后一行`
'         行号从 1 开始
'         如果 `sectionName` 是空字符串，则默认返回 `{-1, -1}`
Public Function getSectionUsageRangeRowNo(ByRef sectionName As String) As PrimitivePair
    ' 1. 检查参数
    ' 1.1 如果 sectionName 是空字符串，则默认返回 {-1, -1}
    If sectionName = "" Then
        Set getSectionUsageRangeRowNo = New PrimitivePair
        getSectionUsageRangeRowNo.Key1 = -1
        getSectionUsageRangeRowNo.Key2 = -1

        Exit Function
    End If

    ' 2. 在 sectionName 所在的列进行检查
    Dim r As Range
    Set r = fieldSheet.Columns(fieldSectionNameColNo) _
        .Find(what:=sectionName, lookat:=xlWhole)

    ' 2.1 搜索指定内容所在的起始行号
    Dim startRowNo As Long
    If r Is Nothing Then
        ' 如果无法在指定列上搜索到字符串，则: `起始行号 = -1` ，`结束行号 = -1`
        Set getSectionUsageRangeRowNo = New PrimitivePair
        getSectionUsageRangeRowNo.Key1 = -1
        getSectionUsageRangeRowNo.Key2 = -1

        Exit Function
    End If

    startRowNo = r.Row

    ' 2.2 获取最后一个内容单元的起始行号
    Dim tempRowNo As Long
    tempRowNo = LastSectionStartRowNo

    ' 2.3 检查，如果`起始行号`就是最后一个内容单元的起始行号，则将会设置 `结束行号 = 当前sheet已使用区域的最后一行`
    If startRowNo = tempRowNo Then
        Set getSectionUsageRangeRowNo = New PrimitivePair
        getSectionUsageRangeRowNo.Key1 = startRowNo
        getSectionUsageRangeRowNo.Key2 = UtilsWorksheet.getUsedRangeEndRowNo(fieldSheet)

        Exit Function
    End If

    ' 2.4 否则，从 起始行号 开始向下搜索已使用区域的结束位置
    '    这里可以保证指定类的从 起始行号 ～ 当前sheet已使用区域的最后一行 之间是存在其他非空单元格的
    tempRowNo = fieldSheet.Cells(startRowNo, fieldSectionNameColNo).End(xlDown).Row - 1

    Set getSectionUsageRangeRowNo = New PrimitivePair
    getSectionUsageRangeRowNo.Key1 = startRowNo
    getSectionUsageRangeRowNo.Key2 = tempRowNo
End Function

' sectionName 所在的内容单元的结束行号
'    这里会从其开始行的下一行开始搜索，下一个非空单元格的前一行作为结束行号
'    开始位置 就是 sectionName 在 `SectionNameColNo` 列的第一次出现的行号
'    如下，搜索的字符串为 `key1`，则返回的结果为 `12`
'        ```
'        行号  列N
'        10    str1  <<<< 从这里开始
'        11
'        12          <<<< 到这里结束，并返回这个行号
'        13    str2
'        ```
' @param sectionName 需要搜索的内容单元
'            如果 `sectionName` 是空字符串，则默认返回 `-1`
' @return 结束行号
'             如果无法在指定列上搜索到字符串，则: `-1`
'             如果`起始行号`就是指定列上被使用的最后一行，则将会设置 `结束行号 = 当前sheet已使用区域的最后一行`
'         行号从 1 开始
'         如果 `sectionName` 是空字符串，则默认返回 `-1`
Public Function getSectionUsageLastRowNo(ByRef sectionName As String) As Long
    getSectionUsageLastRowNo = getSectionUsageRangeRowNo(sectionName).Key2
End Function

' 获取一个内容单元的某一列上的内容
'
' @param sectionName 内容单元名
' @optional-param startCol 获取内容时的起始列号（数组型）或 起始列名（字符串型）
'                 默认值 = `-1`
'                 当值是 -1 时，会从 `ContentStartColNo` 列获取
' @return sectionName 所在的内容单元的某一列上的内容
'                      如果 sectionName 不存在，则返回空字符串
'                      如果内容单元的指定列上不存在数据，则返回空字符串
'                      获取内容时，将会从内容单元的第一个非空单元格开始获取
'                      当前遇到一个空单元格时，不会停止，而是会继续遍历到这个内容单元的结束行号。当遇到多个空单元格时，会在结果中增加一个空行，然后再继续添加后面的内容
Public Function getSectionContentInCol(ByRef sectionName As String, _
    Optional ByRef startCol As Variant = -1) As String
    ' ==================================================
    ' 1. 获取 sectionName 所在的内容单元的使用范围
    ' ==================================================
    Dim rangeRowNo As PrimitivePair
    Set rangeRowNo = getSectionUsageRangeRowNo(sectionName)

    Dim startRowNo As Long
    Dim endRowNo As Long
    startRowNo = rangeRowNo.Key1
    endRowNo = rangeRowNo.Key2

    ' 检查: 如果这个内容单元不存在，则退出，并返回空字符串
    If startRowNo = -1 Then
        Exit Function
    End If

    ' ==================================================
    ' 2. 计算列号
    ' ==================================================
    ' 1. 计算搜索的列号
    Dim targetColNo As Integer
    If startCol = -1 Then
        targetColNo = sectionContentStartColNo
    Else
        targetColNo = UtilsWorksheet.convertVariantColToColNo(startCol)
    End If

    With fieldSheet
        ' ==================================================
        ' 3. 搜索起始行号
        ' ==================================================
        ' 1. 获取起始行号
        Dim contentStartRowNo As Long
        contentStartRowNo = .Cells(startRowNo, targetColNo).End(xlDown).Row

        ' 2. 检查: 如果起始行号超过了结束行号，则退出，并返回空字符串
        If contentStartRowNo > endRowNo Then
            Exit Function
        End If

        ' ==================================================
        ' 4. 连接第一组连续的字符串
        ' ==================================================
        ' 1. 连接第一行
        getSectionContentInCol = .Cells(startRowNo, contentStartRowNo).Value

        ' 2. 连接剩余的行
        Dim i As Long
        Dim cellText
        For i = contentStartRowNo To endRowNo
            cellText = .Cells(i, targetColNo).Text

            ' 检查: 如果是空行，则退出遍历
            If cellText = "" Then
                Exit For
            End If

            getSectionContentInCol = getSectionContentInCol & vbCrLf & cellText
        Next

        ' ==================================================
        ' 5. 处理剩余的内容
        ' ==================================================
        While i <= endRowNo
            ' 1. 获取起始行号
            contentStartRowNo = .Cells(i, targetColNo).End(xlDown).Row

            ' 2. 检查: 如果起始行号超过了结束行号，则退出，并返回空字符串
            If contentStartRowNo > endRowNo Then
                Exit Function
            End If

            ' 3. 连接一个空行
            getSectionContentInCol = getSectionContentInCol & vbCrLf

            ' 4. 连接后面的字符串
            For i = contentStartRowNo To endRowNo
                cellText = .Cells(i, targetColNo).Text

                ' 检查: 如果是空行，则退出遍历
                If cellText = "" Then
                    Exit For
                End If

                getSectionContentInCol = getSectionContentInCol & vbCrLf & cellText
            Next
        Wend
    End With
End Function

' --------------------------------------------------
'  写入数据
' --------------------------------------------------
' 在一个指定行上写入 sectionName 字符串
' 在这一行的 `SectionNameColNo` 列上写入 sectionName 的内容，并可以设置行的颜色
'
' @param rowNo 写入关键字的行
' @param sectionName 内容单元名
' @optional-param rowBgColorNo 整行的背景色
'                              默认值 = `-1`
'                              如果这个值是负数，则不设置背景色
Public Sub writeSectionName(ByRef rowNo As Long, ByRef sectionName As String, Optional ByRef rowBgColorNo As Long = -1)
    With fieldSheet
        ' 写入数据
        .Cells(rowNo, fieldSectionNameColNo).Value = sectionName

        ' 设置背景色
        If rowBgColorNo > 0 Then
            .Rows(rowNo).Interior.Color = rowBgColorNo
        End If
    End With
End Sub

' 向指定单元格中写入数据
' @param rowNo 行号
' @param colNo 列号
' @param val 需要写入单元格的数据
Public Sub writeCell(ByRef rowNo As Long, ByRef colNo As Integer, ByRef val As Variant)
    fieldSheet.Cells(rowNo, colNo).Value = val
End Sub

' 向指定位置插入一个 DataFrame 数据
' 插入时，如果发现 df 的输出范围内（整个行的范围）存在已经使用的单元格，会主动向下移动那些单元格
'
' @param startRowNo 写出 df 的起始行号
' @param df DataFrame 数据
'           如果这个参数是 `Nothing`，则不执行任何操作
' @optional-param needBorder 写出后的数据是否需要设置边框
' @optional-param needWriteTitle 写出数据时，是否需要写出标题
' @optional-param titleBgColorNo 标题的背景色
'                                只在 `needWriteTitle == True` 时生效
'                                如果参数值是负数，则不设置表头背景色
' @optional-param startCol 写出的起始列号（数组型）或 起始列名（字符串型）
'                          默认值 = `-1`
'                          当值是 -1 时，会将空表格从 `ContentStartColNo` 列开始写入
Public Sub appendDataFrame( _
    ByRef startRowNo As Long, ByRef df As IDataFrame, _
    Optional ByRef needBorder As Boolean = True, _
    Optional ByRef needWriteTitle As Boolean = True, _
    Optional ByRef titleBgColorNo As Long = -1, _
    Optional ByRef startCol As Variant = -1)
    ' 1. 检查参数
    ' 1.1 检查 df
    ' 1.1.1 如果 df 为空，则立刻结束
    If df Is Nothing Then
        Exit Sub
    End If

    ' 1.1.2 如果 df 中没有列，则立刻结束
    If df.ColumnCount = 0 Then
        Exit Sub
    End If

    ' 1.2 检查需要输出的行数
    ' 1.2.1 累计输出的行数
    Dim lineCount As Long
    lineCount = df.Size

    If needWriteTitle  Then
        ' 如果需要标题，则输出的行数 + 1
        lineCount = lineCount + 1
    End If

    ' 1.2.2 如果需要输出的行数为 0，则立刻结束
    If lineCount = 0 Then
        Exit Sub
    End If

    ' 1.3 计算列号
    Dim realStartColNo As Integer
    If startCol = -1 Then
        realStartColNo = sectionContentStartColNo
    Else
        realStartColNo = UtilsWorksheet.convertVariantColToColNo(startCol)
    End If

    ' 2. 从起始行开始分配 lineCount 行空行
    allocateTextRow startRowNo, lineCount

    ' 3. 写出 df
    doWriteDataFrame startRowNo, realStartColNo, df, _
        needBorder, needWriteTitle, titleBgColorNo

End Sub

' 向指定位置插入一个 DataFrame 数据
' 插入时，如果发现 df 的输出范围内（整个行的范围）存在已经使用的单元格，会主动向下移动那些单元格
'
' @param startRowNo 写出 df 的起始行号
' @param df DataFrame 数据
'           如果这个参数是 `Nothing`，则不执行任何操作
' @optional-param needBorder 写出后的数据是否需要设置边框
' @optional-param needWriteTitle 写出数据时，是否需要写出标题
' @optional-param titleBgColorNo 标题的背景色
'                                只在 `needWriteTitle == True` 时生效
'                                如果参数值是负数，则不设置表头背景色
' @optional-param startCol 写出的起始列号（数组型）或 起始列名（字符串型）
'                          默认值 = `-1`
'                          当值是 -1 时，会将空表格从 `ContentStartColNo` 列开始写入
Public Sub writeDataFrame( _
    ByRef startRowNo As Long, ByRef df As IDataFrame, _
    Optional ByRef needBorder As Boolean = True, _
    Optional ByRef needWriteTitle As Boolean = True, _
    Optional ByRef titleBgColorNo As Long = -1, _
    Optional ByRef startCol As Variant = -1)
    ' 1. 检查参数
    ' 1.1 检查 df
    ' 1.1.1 如果 df 为空，则立刻结束
    If df Is Nothing Then
        Exit Sub
    End If

    ' 1.1.2 如果 df 中没有列，则立刻结束
    If df.ColumnCount = 0 Then
        Exit Sub
    End If

    ' 1.2 检查需要输出的行数
    ' 1.2.1 累计输出的行数
    Dim lineCount As Long
    lineCount = df.Size

    If needWriteTitle  Then
        ' 如果需要标题，则输出的行数 + 1
        lineCount = lineCount + 1
    End If

    ' 1.2.2 如果需要输出的行数为 0，则立刻结束
    If lineCount = 0 Then
        Exit Sub
    End If

    ' 1.3 计算列号
    Dim realStartColNo As Integer
    If startCol = -1 Then
        realStartColNo = sectionContentStartColNo
    Else
        realStartColNo = UtilsWorksheet.convertVariantColToColNo(startCol)
    End If

    ' 2. 写出 df
    doWriteDataFrame startRowNo, realStartColNo, df, _
        needBorder, needWriteTitle, titleBgColorNo

End Sub

' 写出一个 DataFrame 的数据
Private Sub doWriteDataFrame( _
    ByRef startRowNo As Long, ByRef startColNo As Integer, ByRef df As IDataFrame, _
    ByRef needBorder As Boolean, _
    ByRef needWriteTitle As Boolean, _
    ByRef titleBgColorNo As Long)

    ' 1. 写出 df 的数据
    df.printToWorksheet fieldSheet, _
                        sheetWriteRowNo:= startRowNo, _
                        sheetWriteColNo:= startColNo, _
                        needColName:= needWriteTitle

    With fieldSheet
        ' 2. 设置标题背景色
        '    只在需要写出标题 + 标题背景色号不是负数时设置
        If needWriteTitle Then
            If titleBgColorNo >= 0 Then
                .Range( _
                    .Cells(startRowNo, startColNo), _
                    .Cells(startRowNo, startColNo + df.ColumnCount - 1) _
                ).Interior.Color = titleBgColorNo
            End If
        End If

        ' 3. 设置表格的边框
        If needBorder Then
            With fieldSheet
                UtilsWorksheet.setRangeAllDefaultBorders .Range( _
                        .Cells(startRowNo, startColNo), _
                        .Cells(startRowNo + df.Size, startColNo + df.ColumnCount - 1) _
                    )
            End With
        End If
    End With

End Sub

' 清空指定内容单元内的内容，然后将字符串写入内容单元。默认从 sectionName 行的下一行开始写入
' 写入时，如果发现内容单元内的行数不足，将会自动增加
'
' @param sectionName 内容单元名
' @param str 需要写入的内容
' @optional-param lineCount 写入 `str` 时需要的行数
'                           如果参数值不是一个正数，则会使用 `vbLf` 对 `str` 进行分割，并将分割后的行数作为写入时的行数
'                           如果手动设置了 `lineCount`，但是 `lineCount` 比 str 的行数少，写入的内容可能会覆盖后面内容单元中的内容，需要注意
' @optional-param minLineCount 写入字符串后，内容单元的最小行数
'                              当 `lineCount + 1 < minLineCount` 时，将会保证内容单元中有 `minLineCount` 行
' @optional-param needClearAll 是否需要清除内容单元中的所有内容
'                              默认值 = `False`，只会清除 `ContentStartColNo` 列的内容
Public Sub overwriteStrInSection(ByRef sectionName As String, ByRef str As String, _
    Optional ByVal lineCount As Long = 0, _
    Optional ByVal minLineCount As Long = 8, _
    Optional ByVal needClearAll As Boolean = False)

    ' ==================================================
    ' 1. 获取 sectionName 所在的内容单元的使用范围
    ' ==================================================
    Dim rangeRowNo As PrimitivePair
    Set rangeRowNo = getSectionUsageRangeRowNo(sectionName)

    Dim startRowNo As Long
    Dim endRowNo As Long
    startRowNo = rangeRowNo.Key1
    endRowNo = rangeRowNo.Key2

    ' 检查: 如果这个内容单元不存在，则退出
    If startRowNo = -1 Then
        Exit Sub
    End If

    ' 将起始行定位到 sectionName 行的下一行
    startRowNo = startRowNo + 1

    ' ==================================================
    ' 2. 清空内容单元
    ' ==================================================
    With fieldSheet
        ' 如果结束行没有在起始行的后面，则说明当前内容单元内没有行，所以不需要做清除
        If endRowNo > startRowNo Then
            If needClearAll Then
                ' 清除整个内容单元
                UtilsWorksheet.clearRangeAndToText .Range(startRowNo & ":" & endRowNo)
            Else
                ' 只清除 sectionContentStartColNo 列的内容
                UtilsWorksheet.clearRangeAndToText .Range(.Cells(startRowNo, sectionContentStartColNo), .Cells(endRowNo, sectionContentStartColNo))
            End If
        End If
    End With

    ' ==================================================
    ' 3. 计算、调整行数
    ' ==================================================
    Dim needLineCount As Long
    ' 1. 检查: 如果设置了参数 lineCount，则设置 `needLineCount = lineCount`
    If lineCount > 0 Then
        needLineCount = lineCount
    ' 2. 否则
    Else
        ' 2.1 使用 vbCrLf 分割字符串
        Dim strSplitor As Variant
        ' 2.2 计算分割后的行数 strLineCount
        strSplitor = split(str)
        Dim strLineCount As Integer
        strLineCount = UBound(strSplitor) - LBound(strSplitor) + 1

        ' 2.3 检查: 如果行数小于 = 1 ，则设置为 needLineCount=1
        If strLineCount <= 1 Then
            needLineCount = 1
        ' 2.4 否则，设置 `needLineCount = strLineCount`
        Else
            needLineCount = strLineCount
        End If
    End If

    ' 3. 处理最小行数
    ' 3.1 检查: 如果 needLineCount + 1 <= minLineCount，则设置: needLineCount = minLineCount
    If (needLineCount + 1) <= minLineCount Then
        needLineCount = minLineCount

    ' 3.2 否则，设置为 needLineCount = needLineCount + 1
    Else
        needLineCount = needLineCount + 1
    End If

    ' 4. 计算已有的行数 (从 key 行的下一行开始)
    Dim currentLineCount As Integer
    currentLineCount = endRowNo - startRowNo + 1

    ' 5. 分配行
    ' 5.1 检查: 如果 needLineCount < currentLineCount，则裁剪多余的行
    ' 5.2 否则，检查: 如果 needLineCount > currentLineCount，则分配多余的行
    ' 5.3. 检查: 如果已有的行过多，则删除
    Dim i As Long
    If needLineCount < currentLineCount Then
        ' 这里必须 + 1，因为删除一行时，就是 endRowNo:endRowNo 这一行，否则就变成删除 2 行了
        fieldSheet _
            .Rows((endRowNo - (currentLineCount - needLineCount) + 1) & ":" & endRowNo) _
            .Delete

    ' 5.4. 否则，检查: 如果已有的行过少，则需要增加
    ElseIf needLineCount > currentLineCount Then
        With fieldSheet.Rows(endRowNo)
            For i = 1 To (needLineCount - currentLineCount)
                .Insert
            Next
        End With
    End If

    ' ==================================================
    ' 4. 写入字符串
    ' ==================================================
    ' 1. 粘贴格式化后的 sql
    UtilsWorksheet.pasteStrToCell fieldSheet, str, startRowNo, sectionContentStartColNo
End Sub


' 在一个内容单元中写入一个 DataFrame 数据
' 如果指定的内容单元不存在，则会自动创建
'
' @param sectionName 内容单元名
' @param df DataFrame 数据
'           如果这个参数是 `Nothing`，则不执行任何操作
' @optional-param needBorder 写出后的数据是否需要设置边框
' @optional-param needWriteTitle 写出数据时，是否需要写出标题
' @optional-param titleBgColorNo 标题的背景色
'                 只在 `needWriteTitle == True` 时生效
'                 如果参数值是负数，则不设置表头背景色
' @optional-param lastSectionName 上一个内容单元名
'                 如果 `sectionName` 指定的内容单元不存在，则会获取 `lastSectionName` 参数指定的内容的结束行，并在 `lastSectionName` 的结束行后，新增这个 `sectionName` 内容单元
'                 如果 `lastSectionName` 是空字符串，则会把新的内容单元添加到 sheet 的已使用的最后一行的后面
' @optional-param needReserveOldData 如果指定的内容单元下已经有数据了，是否需要保留那些数据
'                  默认会将数据移动到新数据的右侧，并间隔 2 列
Public Sub writeDataFrameInSection( _
    ByRef sectionName As String, _
    ByRef df As IDataFrame, _
    Optional ByRef needBorder As Boolean = True, _
    Optional ByRef needWriteTitle As Boolean = True, _
    Optional ByRef titleBgColorNo As Long = -1, _
    Optional ByRef lastSectionName As String = "", _
    Optional ByVal needReserveOldData As Boolean = False, _
    Optional ByVal bottomMinEmptyRowCount As Integer = 5 _
)
    ' ==================================================
    ' 1. 检查参数
    ' ==================================================
    ' 1. 检查: 如果 df 是空，则立刻结束
    If df Is Nothing Then
        Exit Sub
    End If

    ' ==================================================
    ' 2. 获取 DF 的信息
    ' ==================================================
    ' 1. 获取 df 的行数
    Dim dfRowCount As Long
    dfRowCount = df.Size

    ' 2. 检查: 如果需要表头，则行数需要 + 1
    If needWriteTitle Then
        dfRowCount = dfRowCount + 1
    End If

    ' ==================================================
    ' 3. 尝试获取 sectionName 的内容单元
    ' ==================================================
    ' 1. 获取内容单元的使用范围
    Dim rangeRowNo As PrimitivePair
    Set rangeRowNo = getSectionUsageRangeRowNo(sectionName)

    Dim startRowNo As Long
    Dim endRowNo As Long

    ' 2. 获取内容单元的起始行号
    startRowNo = rangeRowNo.Key1

    ' 正在操作的行号
    Dim workRowNo As Long

    ' ==================================================
    ' 4. 如果不存在 sectionName 的内容单元时，则新建内容单元
    ' ==================================================
    If startRowNo = -1 Then
        ' 1. 获取上一个内容单元的最后一行
        Dim lastSectionEndRowNo As Long
        ' 1.1 检查: 如果 lastSectionName 不是空字符串，则获取这个内容单元的最后一行
        If lastSectionName <> "" Then
            lastSectionEndRowNo = getSectionUsageLastRowNo(lastSectionName)

            ' 如果没有找到这个内容单元，则设置为当前 sheet 的使用范围的最后一行
            If lastSectionEndRowNo = -1 Then
                lastSectionEndRowNo = UtilsWorksheet.getUsedRangeEndRowNo(fieldSheet)
            End If

        ' 1.2 否则 (lastSectionName 是空字符串)，则获取当前 sheet 的使用范围的最后一行
        Else
            lastSectionEndRowNo = UtilsWorksheet.getUsedRangeEndRowNo(fieldSheet)
        End If

        ' 2. 将操作行号定位到内容单元 key 行
        workRowNo = lastSectionEndRowNo + 1

        ' 3. 分配行用来写入内容单元的 key + df + bottomMinEmptyRowCount 个额外的空行
        allocateTextRow workRowNo, dfRowCount + 1 + bottomMinEmptyRowCount

        ' 4. 写入内容单元的key
        writeSectionName workRowNo, sectionName, rowBgColorNo:= UtilsContentFormat.COLOR_ORANGE

        ' 5. 将操作行号定位到内容单元 key 行的下一行
        workRowNo = workRowNo + 1

    ' ==================================================
    ' 5. 如果存在 sectionName 的内容单元时，则写入数据
    ' ==================================================
    Else
        ' 1. 获取内容单元的结束行号
        endRowNo = rangeRowNo.Key2

        ' 2. 将工作行定位到内容单元的 key 行的下一行
        workRowNo = startRowNo + 1

        '   !!!!! 内容单元存在只有 key 行的情况，所以这里是有在内容单元中存在内容行时，
        '         才执行清空或保存的操作
        If endRowNo > startRowNo Then
            ' 3. 检查: 如果不需要保留旧的数据，则直接清空内容单元
            If needReserveOldData = False Then
                UtilsWorksheet.clearRangeAndToText fieldSheet.Rows(workRowNo & ":" & endRowNo)

            ' 4. 否则，移动旧的数据到 DF 数据的右侧
            Else
                ' 4.1 计算需要移动的数据的右边界
                Dim rightBoundaryColNo As Integer
                rightBoundaryColNo = sectionContentStartColNo + df.ColumnCount - 1

                With fieldSheet
                    ' 4.2 将 rightBoundaryColNo 右侧的数据删除
                    UtilsWorksheet.clearRangeAndToText .Range( _
                        .Cells(workRowNo, rightBoundaryColNo), _
                        .Cells(endRowNo, UtilsWorksheet.getUsedRangeEndColumnNo(fieldSheet)) _
                    )

                    ' 4.3 从内容单元中获取从 sectionContentStartColNo 列开始到 rightBoundaryColNo 的数据区域
                    Dim oldDataRange As Range
                    Set oldDataRange = .Range( _
                        .Cells(workRowNo, sectionContentStartColNo), _
                        .Cells(endRowNo, rightBoundaryColNo) _
                    )

                    ' 4.4 将数据区域向右移动，并间隔两列
                    '     + 3 = 粘贴到间隔两列后的第三列
                    UtilsWorksheet.rangeCutAndPaste oldDataRange, .Cells(workRowNo, rightBoundaryColNo + 3)
                End With
            End If
        End If

        ' 5. 分配行
        ' 5.1 计算需要新增的行数
        ' 需要的行数 = dfRowCount + bottomMinEmptyRowCount(个空行)
        ' 现有的行数 = sqlParameterEndRowNo - sqlParameterStartRowNo
        '             这里不需要 + 1，因为需要跳过 section key 行
        Dim needAddRowCount As Long
        needAddRowCount = dfRowCount + bottomMinEmptyRowCount - (endRowNo - startRowNo)

        ' 5.2 检查: 如果需要的行数 > 0，则说明现在 section 内空间不足，则从 endRowNo 开始分配行
        If needAddRowCount > 0 Then
            allocateTextRow endRowNo + 1, needAddRowCount
        End If
    End If

    ' ==================================================
    ' 6. 写入 DF 数据
    ' ==================================================
    writeDataFrame workRowNo, df, _
        needBorder:=needBorder, _
        needWriteTitle:=needWriteTitle, _
        titleBgColorNo:=titleBgColorNo

End Sub

' ==================================================
'  插入行列
' ==================================================
' 从 startRowNo 行开始时计算，分配 `count` 数量的、格式是文字的空行
' 从 startRowNo 行开始向下，如果已经存在了 `count` 行的空行，则不会进行任何处理
' 如果 startRowNo 行 ～  `startRowNo + count - 1`行之间的第 N 行被占用了（存在任何的设置或数据），
'      则从第 N 行开始向下添加空行，添加 `count - (N - startRowNo)` 行，以避免覆盖那些已被占用的行
'
' @param startRowNo 分配的起始行
' @param count 需要分配的行数
' @return 分配之后的结束行行号
Public Function allocateTextRow(ByRef startRowNo As Long, ByRef count As Long) As Long
    allocateTextRow = UtilsWorksheet.allocateTextRow(fieldSheet, startRowNo, count)
End Function

' 在指定行的上面增加 `insertRowCount` 行
' @param startRowNo 起始行号
' @optional-param insertRowCount 需要增加的行数
'                                默认值 = 1 行
'                                如果 `insertRowCount < 1`，则不会执行任何操作
Public Sub insertRowToUpper(ByRef startRowNo As Long, Optional ByRef insertRowCount As Integer = 1)
    UtilsWorksheet.insertRowToUpper fieldSheet, startRowNo, insertRowCount
End Sub

' 在指定行的下面增加 `insertRowCount` 行
' @param startRowNo 起始行号
' @optional-param insertRowCount 需要增加的行数
'                                默认值 = 1 行
'                                如果 `insertRowCount < 1`，则不会执行任何操作
Public Sub insertRowToDown(ByRef startRowNo As Long, Optional ByRef insertRowCount As Integer = 1)
    UtilsWorksheet.insertRowToDown fieldSheet, startRowNo, insertRowCount
End Sub

' ==================================================
'  格式设置
' ==================================================
' 各列的长度自动适应
Public Sub colAutoFit()
    fieldSheet.Cells.EntireColumn.AutoFit
End Sub

' 设置指定行的的背景色
'     方法内部不会检查行号的范围
'
' @param rowNo 行号，从 1 开始
' @param color 颜色编号
Public Sub setRowBackgroundColor(ByRef rowNo As Long, ByRef colorNo As Long)
    fieldSheet.Rows(rowNo).Interior.Color = colorNo
End Sub

' ==================================================
'  获取数据
' ==================================================

' 获取指定列内的数据，并通过分隔符拼接成字符串
' @param colNoOrIndex 列号或列索引，可以使用 String 或 Integer 类型的数据
' @param startRow 起始行号
' @param endRow 结束行号
' @optional-param rowDelimiter 行数据拼接时使用的分隔符。默认值=vbLf
' @optional-param boundaryStr 列的边界字符串，默认值 = `""`
' @optional-param emptyCellStr 保留空单元格时，用来代替的字符串，默认值 = `""`
' @optional-param emptyCellNeedBoundaryStr 保留空单元格时，是否需要附加边界字符串，默认值 = `False`
' @optional-param needEmptyRow 是否保留空行，默认值 = `True`
'                              这里和 `needEmptyCell` 无关。如果 `needEmptyRow` 为 `True`，只要这一行为空，就会直接舍弃
' @return 指定列内拼接后的字符串数据
Public Function getColumnDataStr( _
    ByRef colNoOrIndex As Variant, _
    ByRef startRow As Long, ByRef endRow As Long, _
    Optional ByRef rowDelimiter As String = vbLf, _
    Optional ByRef boundaryStr As String = "", _
    Optional ByRef emptyCellStr As String = "", Optional ByRef emptyCellNeedBoundaryStr As Boolean = False, _
    Optional ByRef needEmptyRow As Boolean = True) As String

    getColumnDataStr = UtilsWorksheet.getColumnDataStr( _
        colNoOrIndex, startRow, endRow, _
        sheet:=fieldSheet, _
        rowDelimiter:=rowDelimiter, boundaryStr:=boundaryStr, _
        emptyCellStr:=emptyCellStr, emptyCellNeedBoundaryStr:=emptyCellNeedBoundaryStr, _
        needEmptyRow:=needEmptyRow)
End Function

' ==================================================
'  表格操作
' ==================================================

' 从指定位置开始添加一个空的表格
' 插入时，如果发现表格范围内（整个行的范围）存在已经使用的单元格，会主动向下已经那些单元格
'
' @param startRowNo 写出的起始行号
' @param titleNames 表头数组
' @param tableBodyCount 表格行的行数
'                       如果 `tableBodyCount <= 0`，将会忽略表格行的设置
' @optional-param needBorder 写出后的数据是否需要设置边框
'                            默认值 = True
' @optional-param titleBgColorNo 标题的背景色
'                                默认值 = `-1`
'                                只在 `titleNames` 非空时生效
'                                如果当前参数的值是负数，则不会设置标题的背景色
' @optional-param startCol 写出的起始列号（数组型）或 起始列名（字符串型）
'                          默认值 = `-1`
'                          当值是 -1 时，会将空表格从 `ContentStartColNo` 列开始写入
Public Sub appendEmptyTable(ByRef startRowNo As Long, _
        ByRef titleNames As Variant, _
        ByRef tableBodyCount As Long, _
        Optional ByRef needBorder As Boolean=True, _
        Optional ByRef titleBgColorNo As Long = -1, _
        Optional ByRef startCol As Variant = -1)
    ' ==================================================
    ' 1. 计算最终需要写出的行数 `lineCount`
    ' ==================================================
    Dim lineCount As Long
    ' 1.1. 初始化为 1，表示表头所占用的行数
    lineCount = 1

    ' 1.2. 如果 `tableBodyCount > 0`，`lineCount += tableBodyCount`
    If tableBodyCount > 0 Then
        lineCount = lineCount + tableBodyCount
    End If

    ' ==================================================
    ' 2. 计算列号
    ' ==================================================
    ' 1. 计算起始列号
    Dim realStartColNo As Integer
    If startCol = -1 Then
        realStartColNo = sectionContentStartColNo
    Else
        realStartColNo = UtilsWorksheet.convertVariantColToColNo(startCol)
    End If

    ' 2. 计算结束列号
    Dim endColNo As Integer
    endColNo = realStartColNo + UBound(titleNames) - LBound(titleNames)

    ' ==================================================
    ' 3. 从起始行开始分配 lineCount 行空行
    ' ==================================================
    allocateTextRow startRowNo, lineCount

    ' ==================================================
    ' 4. 写出数据
    ' ==================================================
    With fieldSheet
        With .Range(.Cells(startRowNo, realStartColNo), .Cells(startRowNo, endColNo))
            ' 1. 写出表头的名字
            .Value = titleNames
            ' 2. 设置表头的颜色
            If titleBgColorNo >= 0 Then
                .Interior.Color = titleBgColorNo
            End If
        End With

        ' 3. 设置表格的边框
        If needBorder Then
            UtilsWorksheet.setRangeAllDefaultBorders .Range( _
                    .Cells(startRowNo, realStartColNo), _
                    .Cells(startRowNo + lineCount - 1, endColNo) _
                )
        End If
    End With
End Sub

' 从指定位置开始写入一个空的表格
'
' @param startRowNo 写出的起始行号
' @param titleNames 表头数组
' @param tableBodyCount 表格行的行数
'                       如果 `tableBodyCount <= 0`，将会忽略表格行的设置
' @optional-param needBorder 写出后的数据是否需要设置边框
'                            默认值 = True
' @optional-param titleBgColorNo 标题的背景色
'                                默认值 = `-1`
'                                只在 `titleNames` 非空时生效
'                                如果当前参数的值是负数，则不会设置标题的背景色
' @optional-param startCol 写出的起始列号（数组型）或 起始列名（字符串型）
'                          默认值 = `-1`
'                          当值是 -1 时，会将空表格从 `ContentStartColNo` 列开始写入
Public Sub writeEmptyTable(ByRef startRowNo As Long, _
        ByRef titleNames As Variant, _
        ByRef tableBodyCount As Long, _
        Optional ByRef needBorder As Boolean=True, _
        Optional ByRef titleBgColorNo As Long = -1, _
        Optional ByRef startCol As Variant = -1)
    ' ==================================================
    ' 1. 计算最终需要写出的行数 `lineCount`
    ' ==================================================
    Dim lineCount As Long
    ' 1.1. 初始化为 1，表示表头所占用的行数
    lineCount = 1

    ' 1.2. 如果 `tableBodyCount > 0`，`lineCount += tableBodyCount`
    If tableBodyCount > 0 Then
        lineCount = lineCount + tableBodyCount
    End If

    ' ==================================================
    ' 2. 计算列号
    ' ==================================================
    ' 1. 计算起始列号
    Dim realStartColNo As Integer
    If startCol = -1 Then
        realStartColNo = sectionContentStartColNo
    Else
        realStartColNo = UtilsWorksheet.convertVariantColToColNo(startCol)
    End If

    ' 2. 计算结束列号
    Dim endColNo As Integer
    endColNo = realStartColNo + UBound(titleNames) - LBound(titleNames)

    ' ==================================================
    ' 3. 写出数据
    ' ==================================================
    With fieldSheet
        With .Range(.Cells(startRowNo, realStartColNo), .Cells(startRowNo, endColNo))
            ' 1. 写出表头的名字
            .Value = titleNames
            ' 2. 设置表头的颜色
            If titleBgColorNo >= 0 Then
                .Interior.Color = titleBgColorNo
            End If
        End With

        ' 3. 设置表格的边框
        If needBorder Then
            UtilsWorksheet.setRangeAllDefaultBorders .Range( _
                    .Cells(startRowNo, realStartColNo), _
                    .Cells(startRowNo + lineCount - 1, endColNo) _
                )
        End If
    End With
End Sub

' 将一个 key-value 写入某个内容单元下的 key-value 表中
' 如果 key 在 key-value 表中已经存在了，将会覆盖旧的 value。如果 key 在 key-value 表中不存在，则会在表的最后一行后新增
'
' 注意事项
'     如果内容单元不存在，将不会执行任何操作
'
' @param sectionName 内容单元的名称
' @param key 需要在内容单元下搜索的 key
' @param val 需要写出的数据
' @optional-param needBorder 当 key 不存在，需要添加到表格时，是否需要设置边框
Public Sub writeKVInSectionKVTable( _
    ByRef sectionName As String, ByRef key As String, ByRef val As Variant, _
    Optional ByRef needBorder As Boolean=True)

    ' ==================================================
    ' 1. 获取内容单元范围
    ' ==================================================
    ' 1. 获取内容单元范围
    Dim rangeRowNo As PrimitivePair
    Set rangeRowNo = getSectionUsageRangeRowNo(sectionName)

    ' 2. 检查: 如果内容单元不存在，则退出处理
    Dim startRowNo As Long
    startRowNo = rangeRowNo.Key1
    If startRowNo = -1 Then
        Exit Sub
    End If

    Dim endRowNo As Long
    endRowNo = rangeRowNo.Key2

    With fieldSheet
        ' ==================================================
        ' 2. 搜索 key 所在的 Range
        ' ==================================================
        Dim keyRange As Range

        ' startRowNo + 1 --> 跳过 sectionName 行
        Set keyRange = .Range( _
                .Cells((startRowNo + 1), sectionContentStartColNo), _
                .Cells(endRowNo, sectionContentStartColNo) _
            ) _
            .Find(what:=key, lookat:=xlWhole)

        ' ==================================================
        ' 3. 检查: 如果 key 不存在
        ' ==================================================
        If keyRange Is Nothing Then
            ' 1 从 config 区域的起始位置开始搜索第一个空白行
            Dim newRowNo As Long
            newRowNo = UtilsWorksheet.nextRowNoWithNoEmptyCell( _
                .Cells(startRowNo + 1, sectionContentStartColNo), _
                endRowNo)

            ' 2. 获取写入数据的目标行
            ' 2.1 如果没有搜素到空白行，说明内容单元已经被占满，必须插入一个新的行
            ' 在这里连续插入两个空白行
            If newRowNo = -1 Then
                newRowNo = endRowNo + 1
                .Rows(newRowNo).Insert
                .Rows(newRowNo).Insert

            ' 2.2 如果 [空白单元格] 与 [结束行] 之间存在的空白行只有 1 个，则需要插入一个新的行
            '     以保证写入数据后，和底部有一个空海
            ElseIf endRowNo - newRowNo <= 1 Then
                .Rows(newRowNo).Insert
            End If

            ' 3. 写入 key
            .Cells(newRowNo, sectionContentStartColNo).Value = key

            ' 4. 写入 value
            .Cells(newRowNo, sectionContentStartColNo + 1).Value = val

            ' 5. 设置边框
            If needBorder Then
                UtilsWorksheet.setRangeAllDefaultBorders .Range( _
                        .Cells(newRowNo, sectionContentStartColNo), _
                        .Cells(newRowNo, sectionContentStartColNo + 1) _
                    )
            End If

        ' ==================================================
        ' 4. 否则，key 存在，则直接写入数据
        ' ==================================================
        Else
            .Cells(keyRange.Row, sectionContentStartColNo + 1).Value = val
        End If

    End With

End Sub


' 从 `sectionName` 开始内容单元下的 key-value 表格中获取 `key` 对应的数据
'
' 注意事项
'     如果没有找到 `sectionName` 开始的内容单元，将会返回 `defaultVal`
'     如果 key 对应的值是空字符串，将会返回 `defaultVal`
'
' @param sectionName 内容单元的名称
' @param key 需要在内容单元下搜索的 key
' @optional-param defaultVal 没有找到 key 对应的数据时，返回的默认值
'                            默认值 = 空字符串
' @optional-param skipTitleCount 搜索 key 时，需要跳过的表头行数
'                                默认值 = 1 (1行表头)
' @return key 对应的数据的字符串
Public Function getValueFromSectionKVTable( _
    ByRef sectionName As String, ByRef key As String, _
    Optional ByVal defaultVal As String = "", _
    Optional ByVal skipTitleCount As Integer = 1) As String

    ' ==================================================
    ' 1. 获取搜索 key 的范围
    ' ==================================================
    ' 1. 获取内容单元的使用范围
    Dim rangeRowNo As PrimitivePair
    Set rangeRowNo = getSectionUsageRangeRowNo(sectionName)

    ' 2. 检查: 如果内容单元不存在，则退出，并返回默认值
    Dim startRowNo As Long
    startRowNo = rangeRowNo.Key1
    If startRowNo = -1 Then
        getValueFromSectionKVTable = defaultVal
        Exit Function
    End If

    ' 3. 获取结束行号
    Dim endRowNo As Long
    endRowNo = rangeRowNo.Key2

    ' 4. 将开始行号定位到 key-value table 的第一行
    '    + 1 表示跳过 section 的 key 行
    startRowNo = startRowNo + 1 + skipTitleCount

    ' ==================================================
    ' 2. 搜索 key 的范围
    ' ==================================================
    With fieldSheet
        ' 1. 搜索 key 的位置
        Dim keyRange As Range
        Dim valueColNo As Integer
        valueColNo = sectionContentStartColNo + 1
        Set keyRange = .Range( _
                .Cells(startRowNo, sectionContentStartColNo), _
                .Cells(endRowNo, valueColNo) _
            ) _
            .Find(what:=key, lookat:=xlWhole)

        ' 2. 检查: 如果 key 不存在，则返回 defaultVal
        If keyRange Is Nothing Then
            getValueFromSectionKVTable = defaultVal
            Exit Function
        End If

        ' 3. 获取 key 这一行上的 value
        getValueFromSectionKVTable = .Cells(keyRange.Row, valueColNo).Value

        ' 4. 检查: 如果 value 是空字符串，则返回 defaultVal
        If getValueFromSectionKVTable = "" Then
            getValueFromSectionKVTable = defaultVal
        End If

    End With
End Function

' 从 `sectionName` 开始内容单元下的 key-value 表格中获取 `key` 对应的数据
'
' 注意事项
'     如果没有找到 `sectionName` 开始的内容单元，将会返回 `defaultVal`
'     如果 key 对应的值是空字符串，将会返回 `defaultVal`
'     如果 value 本身不是一个数值，将会返回 `0`
'
' @param sectionName 内容单元的名称
' @param key 需要在内容单元下搜索的 key
' @optional-param defaultVal 没有找到 key 对应的数据时，返回的默认值
'                            默认值 = 空字符串
' @optional-param skipTitleCount 搜索 key 时，需要跳过的表头行数
'                                默认值 = 1 (1行表头)
' @return key 对应的数值型数据
Public Function getNumberValueFromSectionKVTable( _
    ByRef sectionName As String, ByRef key As String, _
    Optional ByVal defaultVal As Integer = 0, _
    Optional ByVal skipTitleCount As Integer = 1) As Double

    ' 1. 获取 key 对应的字符串数据
    Dim strVal As String
    strVal = getValueFromSectionKVTable( _
        sectionName, key, _
        "", skipTitleCount)

    ' 2. 检查: 如果空字符串，则返回 defaultVal
    If strVal = "" Then
        getNumberValueFromSectionKVTable = defaultVal
    End If

    ' 3. 转换数据类型
    getNumberValueFromSectionKVTable = UtilsNumber.str2Num(strVal)
End Function

' 从 `sectionName` 开始内容单元下的 key-value 表格中获取所有的 value
'     key-value 表格是只占两列的表。第一列是 key，第二列是 value
'
' 注意事项
'     如果没有找到 `sectionName` 开始的内容单元，将会返回一个长度为 0 的 `Collection`
'     如果 key 是空字符串，则不会吸收那一行的 value
'
' @param sectionName 内容单元的名称
' @optional-param skipTitleCount 搜索 key 时，需要跳过的表头行数
'                                默认值 = 1 (1行表头)
' @return 内容单元下的 key-value 表格中获取所有的 value
Public Function getAllValuesFromSectionKVTable( _
    ByRef sectionName As String, _
    Optional ByVal skipTitleCount As Integer = 1) As Collection

    Set getAllValuesFromSectionKVTable = New Collection

    ' ==================================================
    ' 1. 获取 values 的范围
    ' ==================================================
    ' 1. 获取内容单元的使用范围
    Dim rangeRowNo As PrimitivePair
    Set rangeRowNo = getSectionUsageRangeRowNo(sectionName)

    ' 2. 检查: 如果内容单元不存在，则退出
    Dim startRowNo As Long
    startRowNo = rangeRowNo.Key1
    If startRowNo = -1 Then
        Exit Function
    End If

    ' 3. 获取结束行号
    Dim endRowNo As Long
    endRowNo = rangeRowNo.Key2

    ' 4. 将开始行号定位到 key-value table 的第一行
    '    + 1 表示跳过 section 的 key 行
    startRowNo = startRowNo + 1 + skipTitleCount

    ' ==================================================
    ' 2. 获取所有的 values
    ' ==================================================
    With fieldSheet
        ' 1. 获取 key 所在的区域
        Dim keyRange As Range
        Set keyRange = .Range(.Cells(startRowNo, sectionContentStartColNo), .Cells(endRowNo, sectionContentStartColNo))

        ' 2. 获取 values
        Dim valueColNo As Integer
        valueColNo = sectionContentStartColNo + 1

        Dim keyColumn As Range
        For Each keyColumn In keyRange
            If keyColumn.Value <> "" Then
                getAllValuesFromSectionKVTable.Add .Cells(keyColumn.Row, valueColNo).Value
            End If
        Next
    End With
End Function

' 从 `sectionName` 开始内容单元下的 key-value 表格中获取所有的 key
'     key-value 表格是只占两列的表。第一列是 key，第二列是 value
'
' 注意事项
'     如果没有找到 `sectionName` 开始的内容单元，将会返回一个长度为 0 的 `Collection`
'     如果 key 是空字符串，则会跳过那一行
'
' @param sectionName 内容单元的名称
' @optional-param skipTitleCount 搜索 key 时，需要跳过的表头行数
'                                默认值 = 1 (1行表头)
' @return 内容单元下的 key-value 表格中获取所有的 key
Public Function getAllKeysFromSectionKVTable( _
    ByRef sectionName As String, _
    Optional ByVal skipTitleCount As Integer = 1) As Collection

    Set getAllKeysFromSectionKVTable = New Collection

    ' ==================================================
    ' 1. 获取 values 的范围
    ' ==================================================
    ' 1. 获取内容单元的使用范围
    Dim rangeRowNo As PrimitivePair
    Set rangeRowNo = getSectionUsageRangeRowNo(sectionName)

    ' 2. 检查: 如果内容单元不存在，则退出
    Dim startRowNo As Long
    startRowNo = rangeRowNo.Key1
    If startRowNo = -1 Then
        Exit Function
    End If

    ' 3. 获取结束行号
    Dim endRowNo As Long
    endRowNo = rangeRowNo.Key2

    ' 4. 将开始行号定位到 key-value table 的第一行
    '    + 1 表示跳过 section 的 key 行
    startRowNo = startRowNo + 1 + skipTitleCount

    ' ==================================================
    ' 2. 获取所有的 values
    ' ==================================================
    ' 1. 获取 key 所在的区域
    Dim keyRange As Range
    With fieldSheet
        Set keyRange = .Range(.Cells(startRowNo, sectionContentStartColNo), .Cells(endRowNo, sectionContentStartColNo))
    End With

    ' 2. 获取 key
    Dim valueColNo As Integer
    valueColNo = sectionContentStartColNo + 1

    Dim keyColumn As Range
    Dim keyStr As String
    For Each keyColumn In keyRange
        keyStr = keyColumn.Value
        If keyStr <> "" Then
            getAllKeysFromSectionKVTable.Add keyStr
        End If
    Next
End Function

' 从 `sectionName` 开始内容单元下的 key-value 表格中的 key 和 value 组成的 map
'     key-value 表格是只占两列的表。第一列是 key，第二列是 value
'
' 注意事项
'     如果没有找到 `sectionName` 开始的内容单元，将会返回一个长度为 0 的 `Dictionary`
'     如果 key 是空字符串，则会跳过那一行
'
' @param sectionName 内容单元的名称
' @optional-param skipTitleCount 搜索 key 时，需要跳过的表头行数
'                                默认值 = 1 (1行表头)
' @return 包含内容单元下的 key-value 表格中所有 key 和 value 的 `Dictionary` 对象
Public Function getMapFromSectionKVTable( _
    ByRef sectionName As String, _
    Optional ByVal skipTitleCount As Integer = 1) As Collection

    Set getMapFromSectionKVTable = UtilsCreateSysObj.createMap

    ' ==================================================
    ' 1. 获取 values 的范围
    ' ==================================================
    ' 1. 获取内容单元的使用范围
    Dim rangeRowNo As PrimitivePair
    Set rangeRowNo = getSectionUsageRangeRowNo(sectionName)

    ' 2. 检查: 如果内容单元不存在，则退出
    Dim startRowNo As Long
    startRowNo = rangeRowNo.Key1
    If startRowNo = -1 Then
        Exit Function
    End If

    ' 3. 获取结束行号
    Dim endRowNo As Long
    endRowNo = rangeRowNo.Key2

    ' 4. 将开始行号定位到 key-value table 的第一行
    '    + 1 表示跳过 section 的 key 行
    startRowNo = startRowNo + 1 + skipTitleCount

    ' ==================================================
    ' 2. 获取所有的 values
    ' ==================================================
    ' 1. 获取 key 所在的区域
    With fieldSheet
        Dim keyRange As Range
        Set keyRange = .Range(.Cells(startRowNo, sectionContentStartColNo), .Cells(endRowNo, sectionContentStartColNo))

        ' 2. 获取 key-value
        Dim valueColNo As Integer
        valueColNo = sectionContentStartColNo + 1

        Dim keyColumn As Range
        Dim keyStr As Range
        For Each keyColumn In keyRange
            keyStr = keyColumn.Value
            If keyStr <> "" Then
                getMapFromSectionKVTable.Add keyStr, .Cells(keyColumn.Row, valueColNo).Value
            End If
        Next
    End With
End Function

' 从 `sectionName` 开始内容单元下的 key-multiValue 表格中的 key 和 value 组成的 map
'     key-value 表格是 key 占一列，value 占多列的表。第一列是 key，第二列及以后是 value
'     需要提供生成器参数 `valFunc`，来吸收所有 key 对应的 value，然后生成并返回一个结果来作为保存到结果 map 中的 value
'
' 注意事项
'     如果没有找到 `sectionName` 开始的内容单元，将会返回一个长度为 0 的 `Dictionary`
'     如果 key 是空字符串，则会跳过那一行
' @param sectionName 内容单元的名称
' @param valFunc value 生成器
'                执行时提供的三个参数
'                    1. `Worksheet`, 数据源 sheet
'                    2. `rowNo As Long`, 当前正在处理的 value
'                    3. `startColNo`, 第一个 value 所在的列号
' @optional-param skipTitleCount 搜索 key 时，需要跳过的表头行数
'                                默认值 = 1 (1行表头)
' @return 包含内容单元下的 key-value 表格中所有 key 和 value 的 `Dictionary` 对象
Public Function getMapFromSectionKeyMultiValTable( _
    ByRef sectionName As String, _
    ByRef valFunc As TriFunction, _
    Optional ByVal skipTitleCount As Integer = 1) As Object

    Set getMapFromSectionKeyMultiValTable = UtilsCreateSysObj.createMap

    ' ==================================================
    ' 1. 获取 values 的范围
    ' ==================================================
    ' 1. 获取内容单元的使用范围
    Dim rangeRowNo As PrimitivePair
    Set rangeRowNo = getSectionUsageRangeRowNo(sectionName)

    ' 2. 检查: 如果内容单元不存在，则退出
    Dim startRowNo As Long
    startRowNo = rangeRowNo.Key1
    If startRowNo = -1 Then
        Exit Function
    End If

    ' 3. 获取结束行号
    Dim endRowNo As Long
    endRowNo = rangeRowNo.Key2

    ' 4. 将开始行号定位到 key-value table 的第一行
    '    + 1 表示跳过 section 的 key 行
    startRowNo = startRowNo + 1 + skipTitleCount

    ' ==================================================
    ' 2. 获取所有的 values
    ' ==================================================
    With fieldSheet
        ' 1. 获取 key 所在的区域
        Dim keyRange As Range
        Set keyRange = .Range(.Cells(startRowNo, sectionContentStartColNo), .Cells(endRowNo, sectionContentStartColNo))

        ' 2. 获取 key-value
        Dim keyColumn As Range
        Dim keyStr As String
        For Each keyColumn In keyRange
            keyStr = keyColumn.Value
            If keyStr <> "" Then
                ' 生成 Value，并保存
                getMapFromSectionKeyMultiValTable.Add _
                    keyStr, _
                    valFunc.apply(fieldSheet, keyColumn.Row, sectionContentStartColNo)
            End If
        Next
    End With
End Function
